import { AsyncContext } from './async/context.js'

// Copyright Joyent, Inc. and other Node contributors.
//
// Permission is hereby granted, free of charge, to any person obtaining a
// copy of this software and associated documentation files (the
// "Software"), to deal in the Software without restriction, including
// without limitation the rights to use, copy, modify, merge, publish,
// distribute, sublicense, and/or sell copies of the Software, and to permit
// persons to whom the Software is furnished to do so, subject to the
// following conditions:
//
// The above copyright notice and this permission notice shall be included
// in all copies or substantial portions of the Software.
//
// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS
// OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF
// MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN
// NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM,
// DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR
// OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE
// USE OR OTHER DEALINGS IN THE SOFTWARE.

const R = typeof Reflect === 'object' ? Reflect : null
const ReflectApply = R && typeof R.apply === 'function'
  ? R.apply
  : function ReflectApply (target, receiver, args) {
    return Function.prototype.apply.call(target, receiver, args)
  }

let ReflectOwnKeys
if (R && typeof R.ownKeys === 'function') {
  ReflectOwnKeys = R.ownKeys
} else if (Object.getOwnPropertySymbols) {
  ReflectOwnKeys = function ReflectOwnKeys (target) {
    return Object.getOwnPropertyNames(target)
      .concat(Object.getOwnPropertySymbols(target))
  }
} else {
  ReflectOwnKeys = function ReflectOwnKeys (target) {
    return Object.getOwnPropertyNames(target)
  }
}

function ProcessEmitWarning (warning) {
  if (console && console.warn) console.warn(warning)
}

const NumberIsNaN = Number.isNaN

function EventEmitter () {
  EventEmitter.init.call(this)
}

// Backwards-compat with node 0.10.x
EventEmitter.EventEmitter = EventEmitter

EventEmitter.prototype._events = undefined
EventEmitter.prototype._contexts = undefined
EventEmitter.prototype._eventsCount = 0
EventEmitter.prototype._maxListeners = undefined

// By default EventEmitters will print a warning if more than 10 listeners are
// added to it. This is a useful default which helps finding memory leaks.
let defaultMaxListeners = 10

function checkListener (listener) {
  if (typeof listener !== 'function') {
    throw new TypeError('The "listener" argument must be of type Function. Received type ' + typeof listener)
  }
}

Object.defineProperty(EventEmitter, 'defaultMaxListeners', {
  enumerable: true,
  get: function () {
    return defaultMaxListeners
  },
  set: function (arg) {
    if (typeof arg !== 'number' || arg < 0 || NumberIsNaN(arg)) {
      throw new RangeError('The value of "defaultMaxListeners" is out of range. It must be a non-negative number. Received ' + arg + '.')
    }
    defaultMaxListeners = arg
  }
})

EventEmitter.init = function () {
  if (this._events === undefined ||
      this._events === Object.getPrototypeOf(this)._events) {
    this._events = Object.create(null)
    this._eventsCount = 0
  }

  if (
    this._contexts === undefined ||
    this._contexts === Object.getPrototypeOf(this)._contexts
  ) {
    this._contexts = new Map()
  }

  this._maxListeners = this._maxListeners || undefined
}

// Obviously not all Emitters should be limited to 10. This function allows
// that to be increased. Set to zero for unlimited.
EventEmitter.prototype.setMaxListeners = function setMaxListeners (n) {
  if (typeof n !== 'number' || n < 0 || NumberIsNaN(n)) {
    throw new RangeError('The value of "n" is out of range. It must be a non-negative number. Received ' + n + '.')
  }
  this._maxListeners = n
  return this
}

function _getMaxListeners (that) {
  if (that._maxListeners === undefined) { return EventEmitter.defaultMaxListeners }
  return that._maxListeners
}

EventEmitter.prototype.getMaxListeners = function getMaxListeners () {
  return _getMaxListeners(this)
}

EventEmitter.prototype.emit = function emit (type) {
  const args = []
  for (let i = 1; i < arguments.length; i++) args.push(arguments[i])
  let doError = (type === 'error')

  const events = this._events
  if (events !== undefined) { doError = (doError && events.error === undefined) } else if (!doError) { return false }

  // If there is no 'error' event listener then throw.
  if (doError) {
    let er
    if (args.length > 0) { er = args[0] }
    if (er instanceof Error) {
      // Note: The comments on the `throw` lines are intentional, they show
      // up in Node's output if this results in an unhandled exception.
      throw er // Unhandled 'error' event
    }
    // At least give some kind of context to the user
    const err = new Error('Unhandled error.' + (er ? ' (' + er.message + ')' : ''))
    err.context = er
    throw err // Unhandled 'error' event
  }

  const handler = events[type]

  if (handler === undefined) { return false }

  if (typeof handler === 'function') {
    const context = this._contexts.get(handler)
    if (context) {
      context.run(() => {
        ReflectApply(handler, this, args)
      })
    } else {
      ReflectApply(handler, this, args)
    }
  } else {
    const len = handler.length
    const listeners = arrayClone(handler, len)
    for (let i = 0; i < len; ++i) {
      const context = this._contexts.get(listeners[i])
      if (context) {
        context.run(() => {
          ReflectApply(listeners[i], this, args)
        })
      } else {
        ReflectApply(listeners[i], this, args)
      }
    }
  }

  return true
}

function _addListener (target, type, listener, prepend) {
  let m
  let events
  let existing

  checkListener(listener)

  events = target._events
  if (events === undefined) {
    events = target._events = Object.create(null)
    target._eventsCount = 0
  } else {
    // To avoid recursion in the case that type === "newListener"! Before
    // adding it to the listeners, first emit "newListener".
    if (events.newListener !== undefined) {
      target.emit('newListener', type,
        listener.listener ? listener.listener : listener)

      // Re-assign `events` because a newListener handler could have caused the
      // this._events to be assigned to a new object
      events = target._events
    }
    existing = events[type]
  }

  if (existing === undefined) {
    // Optimize the case of one listener. Don't need the extra array object.
    existing = events[type] = listener
    ++target._eventsCount
  } else {
    if (typeof existing === 'function') {
      // Adding the second element, need to change to array.
      existing = events[type] =
        prepend ? [listener, existing] : [existing, listener]
      // If we've already got an array, just append.
    } else if (prepend) {
      existing.unshift(listener)
    } else {
      existing.push(listener)
    }

    // Check for listener leak
    m = _getMaxListeners(target)
    if (m > 0 && existing.length > m && !existing.warned) {
      existing.warned = true
      // No error code for this since it is a Warning
      // eslint-disable-next-line no-restricted-syntax
      const w = new Error('Possible EventEmitter memory leak detected. ' +
                          existing.length + ' ' + String(type) + ' listeners ' +
                          'added. Use emitter.setMaxListeners() to ' +
                          'increase limit')
      w.name = 'MaxListenersExceededWarning'
      w.emitter = target
      w.type = type
      w.count = existing.length
      ProcessEmitWarning(w)
    }
  }

  target._contexts.set(listener, new AsyncContext.Snapshot())

  return target
}

EventEmitter.prototype.addListener = function addListener (type, listener) {
  return _addListener(this, type, listener, false)
}

/**
 * @instance
 * @method on
 * @memberOf EventEmitter
 * @type {function(any, any): any}
 */
EventEmitter.prototype.on = function on (type, listener) {
  return this.addListener(type, listener)
}

EventEmitter.prototype.prependListener =
    function prependListener (type, listener) {
      return _addListener(this, type, listener, true)
    }

function onceWrapper () {
  if (!this.fired) {
    this.target.removeListener(this.type, this.wrapFn)
    this.fired = true
    if (arguments.length === 0) { return this.listener.call(this.target) }
    return this.listener.apply(this.target, arguments)
  }
}

function _onceWrap (target, type, listener) {
  const state = { fired: false, wrapFn: undefined, target, type, listener }
  const wrapped = onceWrapper.bind(state)
  wrapped.listener = listener
  state.wrapFn = wrapped
  return wrapped
}

EventEmitter.prototype.once = function once (type, listener) {
  checkListener(listener)
  this.on(type, _onceWrap(this, type, listener))
  return this
}

EventEmitter.prototype.prependOnceListener =
    function prependOnceListener (type, listener) {
      checkListener(listener)
      this.prependListener(type, _onceWrap(this, type, listener))
      return this
    }

// Emits a 'removeListener' event if and only if the listener was removed.
EventEmitter.prototype.removeListener =
    function removeListener (type, listener) {
      let position, i, originalListener

      checkListener(listener)

      const events = this._events
      if (events === undefined) { return this }

      const list = events[type]
      if (list === undefined) { return this }

      if (list === listener || list.listener === listener) {
        if (--this._eventsCount === 0) { this._events = Object.create(null) } else {
          delete events[type]
          if (events.removeListener) { this.emit('removeListener', type, list.listener || listener) }
        }
      } else if (typeof list !== 'function') {
        position = -1

        for (i = list.length - 1; i >= 0; i--) {
          if (list[i] === listener || list[i].listener === listener) {
            originalListener = list[i].listener
            position = i
            break
          }
        }

        if (position < 0) { return this }

        if (position === 0) { list.shift() } else {
          spliceOne(list, position)
        }

        if (list.length === 1) { events[type] = list[0] }

        if (events.removeListener !== undefined) { this.emit('removeListener', type, originalListener || listener) }
      }

      this._contexts.delete(listener)

      return this
    }

EventEmitter.prototype.off = function off (type, listener) {
  return this.removeListener(type, listener)
}

EventEmitter.prototype.removeAllListeners =
    function removeAllListeners (type) {
      let i

      const events = this._events
      if (events === undefined) { return this }

      // not listening for removeListener, no need to emit
      if (events.removeListener === undefined) {
        if (arguments.length === 0) {
          this._events = Object.create(null)
          this._eventsCount = 0
        } else if (events[type] !== undefined) {
          if (--this._eventsCount === 0) { this._events = Object.create(null) } else { delete events[type] }
        }
        return this
      }

      // emit removeListener for all listeners on all events
      if (arguments.length === 0) {
        const keys = Object.keys(events)
        let key
        for (i = 0; i < keys.length; ++i) {
          key = keys[i]
          if (key === 'removeListener') continue
          this.removeAllListeners(key)
        }
        this.removeAllListeners('removeListener')
        this._events = Object.create(null)
        this._contexts.clear()
        this._eventsCount = 0
        return this
      }

      const listeners = events[type]

      if (typeof listeners === 'function') {
        this.removeListener(type, listeners)
      } else if (listeners !== undefined) {
        // LIFO order
        for (i = listeners.length - 1; i >= 0; i--) {
          this.removeListener(type, listeners[i])
        }
      }

      return this
    }

function _listeners (target, type, unwrap) {
  const events = target._events

  if (events === undefined) { return [] }

  const evlistener = events[type]
  if (evlistener === undefined) { return [] }

  if (typeof evlistener === 'function') { return unwrap ? [evlistener.listener || evlistener] : [evlistener] }

  return unwrap
    ? unwrapListeners(evlistener)
    : arrayClone(evlistener, evlistener.length)
}

EventEmitter.prototype.listeners = function listeners (type) {
  return _listeners(this, type, true)
}

EventEmitter.prototype.rawListeners = function rawListeners (type) {
  return _listeners(this, type, false)
}

EventEmitter.listenerCount = function (emitter, type) {
  if (typeof emitter.listenerCount === 'function') {
    return emitter.listenerCount(type)
  } else {
    return listenerCount.call(emitter, type)
  }
}

EventEmitter.prototype.listenerCount = function (type) {
  return listenerCount.call(this, type)
}

function listenerCount (type) {
  const events = this._events

  if (events !== undefined) {
    const evlistener = events[type]

    if (typeof evlistener === 'function') {
      return 1
    } else if (evlistener !== undefined) {
      return evlistener.length
    }
  }

  return 0
}

EventEmitter.prototype.eventNames = function eventNames () {
  return this._eventsCount > 0 ? ReflectOwnKeys(this._events) : []
}

function arrayClone (arr, n) {
  const copy = new Array(n)
  for (let i = 0; i < n; ++i) { copy[i] = arr[i] }
  return copy
}

function spliceOne (list, index) {
  for (; index + 1 < list.length; index++) { list[index] = list[index + 1] }
  list.pop()
}

function unwrapListeners (arr) {
  const ret = new Array(arr.length)
  for (let i = 0; i < ret.length; ++i) {
    ret[i] = arr[i].listener || arr[i]
  }
  return ret
}

function once (emitter, name) {
  return new Promise(function (resolve, reject) {
    function errorListener (err) {
      emitter.removeListener(name, resolver)
      reject(err)
    }

    function resolver () {
      if (typeof emitter.removeListener === 'function') {
        emitter.removeListener('error', errorListener)
      }
      resolve([].slice.call(arguments))
    };

    eventTargetAgnosticAddListener(emitter, name, resolver, { once: true })
    if (name !== 'error') {
      addErrorHandlerIfEventEmitter(emitter, errorListener, { once: true })
    }
  })
}

function addErrorHandlerIfEventEmitter (emitter, handler, flags) {
  if (typeof emitter.on === 'function') {
    eventTargetAgnosticAddListener(emitter, 'error', handler, flags)
  }
}

function eventTargetAgnosticAddListener (emitter, name, listener, flags) {
  if (typeof emitter.on === 'function') {
    if (flags.once) {
      emitter.once(name, listener)
    } else {
      emitter.on(name, listener)
    }
  } else if (typeof emitter.addEventListener === 'function') {
    // EventTarget does not have `error` event semantics like Node
    // EventEmitters, we do not listen for `error` events here.
    emitter.addEventListener(name, function wrapListener (arg) {
      // IE does not have builtin `{ once: true }` support so we
      // have to do it manually.
      if (flags.once) {
        emitter.removeEventListener(name, wrapListener)
      }
      listener(arg)
    })
  } else {
    throw new TypeError('The "emitter" argument must be of type EventEmitter. Received type ' + typeof emitter)
  }
}

EventEmitter.EventEmitter = EventEmitter
EventEmitter.once = once

export const Event = globalThis.Event || class Event {}
export const EventTarget = globalThis.EventTarget || class EventTarget {}

export const CustomEvent = globalThis.CustomEvent || class CustomEvent extends Event {
  #detail = null

  constructor (type, options) {
    super(type, options)
    if (options?.detail) {
      this.#detail = options.detail
    }
  }

  get detail () {
    return this.#detail
  }
}

export const MessageEvent = globalThis.MessageEvent || class MessageEvent extends Event {
  #detail = null
  #data = null

  constructor (type, options) {
    super(type, options)
    if (options?.detail) {
      this.#detail = options.detail
    }

    if (options?.data) {
      this.#data = options.data
    }
  }

  get detail () {
    return this.#detail
  }

  get data () {
    return this.#data
  }
}

export const ErrorEvent = globalThis.ErrorEvent || class ErrorEvent extends Event {
  #detail = null
  #error = null

  constructor (type, options) {
    super(type, options)
    if (options?.detail) {
      this.#detail = options.detail
    }

    if (options?.error) {
      this.#error = options.error
    }
  }

  get detail () {
    return this.#detail
  }

  get error () {
    return this.#error
  }
}

export {
  EventEmitter,
  once
}
export default EventEmitter
